﻿/*
 * Environment
 * 
 * initial implementation : 28.5.2011
 *
 * Copyright (c) Adam Sida, asida@centrum.cz.
 * All rights reserved.
 * 
 * for license see Petra.Gis.License.txt
 */
using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
//using System.Linq;
//using System.Text;
using System.Windows.Markup;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using Petra.Gis.DocumentServices;
using Petra.Gis.Support;
using Petra.Gis.ReferenceSystemServices;
using Petra.Gis.ReferenceSystemServices.Transformations;

namespace Petra.Gis
{


  /// <summary>
  /// also represents transformation search engine
  /// that searches in <c>SRSTransformLibrary</c>
  /// for existing/predefined transform.
  /// </summary>
  public static partial class Environment
  {

    private static CoordinateSystem _rSourceCS; //for recursive search
    private static CoordinateSystem _rTargetCS; //for recursive search

    private static CSTPNode root;
    private static List<CSTPNode> _successes;





    #region public Members


    /// <summary>
    /// Creates a transformation between two coordinate systems.
    /// </summary>
    /// <remarks>
    /// This method examines the coordinate systems in order to construct
    /// a transformation between them. This method may fail if no path between 
    /// the coordinate systems is found, using the normal failing behavior of 
    /// the DCP (e.g. throwing an exception).</remarks>
    /// <param name="sourceCS">Source coordinate system</param>
    /// <param name="targetCS">Target coordinate system</param>
    /// <returns>transformation between source and target coordinate system,</returns>	
    public static SRSTransform SearchTransformationBy(CoordinateSystem sourceCS, CoordinateSystem targetCS)
    {

      // search loaded cache for transform...
      TransformList result = _byCoordinateSystems(sourceCS, targetCS);

      if (result == null)
        //throw new ArgumentException("transformation not found");
        return null;

      if (result.Count == 1)
        return result[0];
      else
        //return new ChainedTransform(sourceCS, targetCS, result);
        return new ChainedTransform(result);

    }


    #endregion






    #region private members


    private static TransformList _byCoordinateSystems(CoordinateSystem sourceCS, CoordinateSystem targetCS)
    {
      TransformList result;
      //TransformList result = new TransformList();
      //result = new TransformList();

      // function starts to search from simple case to complex case
      // to avoid cpu time consumption for transforms

      // so first test whether sourceCS and targetCS are same
      // if yes, no transform needed at all (VoidTransform)
      // or only storage unit transform
      if (sourceCS.EqualParams(targetCS, CsCompareType.SkipStorageUnits))
      {
        result = new TransformList();

        if (sourceCS.EqualParams(targetCS, CsCompareType.CompareAllParams))
          result.Add(new VoidTransform(sourceCS, targetCS)); //both are exactly same
        else //storage units transform
          result.Add(new MeasureUnitTransform(sourceCS, targetCS)); //same but different units, provide storage units transform

        return result;
      }

      // ok not same, may be units are different and x,y axes are flipped or different direction of axis ?
      // if both coordinate systems are projected and has x,y axis
      // if axes has different orientation try prepare Coordinate quadrant transform
      // and also resolve units
      if (sourceCS is ProjectedCoordinateSystem && targetCS is ProjectedCoordinateSystem)
      {
        if (sourceCS.EqualParams(targetCS, CsCompareType.SkipProjectionAxesOrientation | CsCompareType.SkipStorageUnits))
        {
          result = _resolveStorageUnitsAndAxes(sourceCS, targetCS);

          //SRSTransform tmp = XYQuadrantTransform.Create(
          //                      (ProjectedCoordinateSystem)sourceCS, 
          //                      (ProjectedCoordinateSystem)targetCS);
          if (result != null)
            return result;
        }
      }

      // ok, source and target are totaly different
      // all next possibilities are more time consuming
      // because requres math transforms and other non simple math equations
      // now search for direct identical transform first...
      result = searchDirectTransform(sourceCS, targetCS);
      if (result != null)
        return result;

      // ok still not found,...
      // if source CS is projected try search for projcs to its geogcs
      // and geogcs to target directly
      if (sourceCS is ProjectedCoordinateSystem &&
          targetCS.EqualParams(((ProjectedCoordinateSystem)sourceCS).GeographicCoordinateSystem, CsCompareType.SkipStorageUnits))
      {
        // now search for projection to spheroid transform
        //SRSTransform tmpTr = projectionToSpheroid((ProjectedCoordinateSystem)sourceCS);
        //TransformList tmpTrSrc = directTransform(sourceCS, ((ProjectedCoordinateSystem)sourceCS).GeographicCoordinateSystem);

        // this call will throw an exception when projection is unsupported by Petra.Gis
        //SRSTransform tmpTrSrc = ProjectionTransform.Create((ProjectedCoordinateSystem)sourceCS, false);
        SRSTransform tmpTrSrc = getProjection((ProjectedCoordinateSystem)sourceCS, false);

        // if any exists try search spheroid to targetcs
        if (tmpTrSrc != null)
        {
          //search direct transform again
          result = searchDirectTransform(((ProjectedCoordinateSystem)sourceCS).GeographicCoordinateSystem, targetCS);
          if (result != null)
          {
            // insert all transforms for source before result
            if (tmpTrSrc is ChainedTransform)
              foreach (SRSTransform t in ((ChainedTransform)tmpTrSrc).CoordinateTransformList)
                result.Insert(0, t);

            else
              result.Insert(0, tmpTrSrc);

            return result;
          }
        }
      }

      // or oposite situation, target is projcs and source is geogcs
      if (targetCS is ProjectedCoordinateSystem &&
        sourceCS.EqualParams(((ProjectedCoordinateSystem)sourceCS).GeographicCoordinateSystem, CsCompareType.SkipStorageUnits))
      {
        //SRSTransform tmpTr = projectionToSpheroid((ProjectedCoordinateSystem)targetCS);
        //TransformList tmpTrTrg = directTransform(((ProjectedCoordinateSystem)targetCS).GeographicCoordinateSystem, targetCS);
        
        //SRSTransform tmpTrTrg = ProjectionTransform.Create((ProjectedCoordinateSystem)targetCS, true);

        SRSTransform tmpTrTrg = getProjection((ProjectedCoordinateSystem)targetCS, true);

        //if (tmpTrTrg != null)
        if (tmpTrTrg != null)
        {
          //search direct transform again
          result = searchDirectTransform(sourceCS, ((ProjectedCoordinateSystem)targetCS).GeographicCoordinateSystem);
          if (result != null)
          {
            // add all transforms for target after result
            if (tmpTrTrg is ChainedTransform)
              foreach (SRSTransform t in ((ChainedTransform)tmpTrTrg).CoordinateTransformList)
                result.Add(t);

            else
              result.Insert(0, tmpTrTrg);

            return result;
          }
        }
      }

      // ok still not found
      // try this:
      // if source and target is projcs, find their projcs to geogcs transforms
      // if both projections supported (no error) then search direct transform between their geogcs
      // this is most common case when map display
      if (sourceCS is ProjectedCoordinateSystem && targetCS is ProjectedCoordinateSystem)
      {
        //SRSTransform tmpTrSrc = projectionToSpheroid((ProjectedCoordinateSystem)sourceCS);
        //SRSTransform tmpTrTrg = projectionToSpheroid((ProjectedCoordinateSystem)targetCS);
        //TransformList tmpTrSrc = directTransform(sourceCS, ((ProjectedCoordinateSystem)sourceCS).GeographicCoordinateSystem);
        //TransformList tmpTrTrg = directTransform(((ProjectedCoordinateSystem)targetCS).GeographicCoordinateSystem, targetCS);

        //SRSTransform tmpTrSrc = ProjectionTransform.Create((ProjectedCoordinateSystem)sourceCS, false);
        //SRSTransform tmpTrTrg = ProjectionTransform.Create((ProjectedCoordinateSystem)targetCS, true);

        SRSTransform tmpTrSrc = getProjection((ProjectedCoordinateSystem)sourceCS, false);
        SRSTransform tmpTrTrg = getProjection((ProjectedCoordinateSystem)targetCS, false);

        if (tmpTrSrc != null && tmpTrTrg != null)
        {
          //search direct transform again
          result = searchDirectTransform(((ProjectedCoordinateSystem)sourceCS).GeographicCoordinateSystem, ((ProjectedCoordinateSystem)targetCS).GeographicCoordinateSystem);
          if (result != null)
          {
            // insert all transforms for source before result
            if (tmpTrSrc is ChainedTransform)
              //foreach (SRSTransform t in ((ChainedTransform)tmpTrSrc).CoordinateTransformList)
              //  result.Insert(0, t);
              result.InsertRange(0, ((ChainedTransform)tmpTrSrc).CoordinateTransformList);

            else
              result.Insert(0, tmpTrSrc);

            // and all transforms for target after result
            if (tmpTrTrg is ChainedTransform)
              foreach (SRSTransform t in ((ChainedTransform)tmpTrTrg).CoordinateTransformList)
                result.Add(t);

            else
              result.Add(tmpTrTrg);

            return result;
          }
          //else
          //  //no result
          //  return null;
        }
      }

      // damn,... ok, worst case, no simple way to find transform
      // must try to use long recursive search....

      //return null;
      //throw new NotImplementedException("sorry transform between different CS is not supported now");

      _successes = new List<CSTPNode>();

      // _sourceCS & _targetCS are used as global vars
      // for recurse calls
      // flip coordinate systems
      // this is because search engine returns transform path in reverse order
      CoordinateSystem tmpCS = _rSourceCS;
      _rSourceCS = targetCS;
      _rTargetCS = tmpCS;

      root = new CSTPNode(null, null);

      //recursive search...
      treeSearchTransform(root, null);

      if (_successes.Count == 0)
        return null;
      else
      {
        //sort result by level, smallest level = less transforms in path
        CSTPNode[] tmp = _successes.ToArray();
        Array.Sort(tmp);

        //put whole coord transf path to list
        result = new TransformList();
        CSTPNode tmp1 = tmp[0];
        result.Add(tmp1.transform);
        while (tmp1.Parent != null)
        {
          result.Add(tmp1.Parent.transform);
          tmp1 = tmp1.Parent;
        }

        //check whether storage unist of start and end transform
        //corresponds with source and target CS
        if (result[0].Source.EqualParams(sourceCS, CsCompareType.CompareAllParams))
          result.Insert(0, new MeasureUnitTransform(result[0].Source, sourceCS));

        if (result[result.Count - 1].Target.EqualParams(targetCS, CsCompareType.CompareAllParams))
          result.Insert(0, new MeasureUnitTransform(result[result.Count - 1].Target, targetCS));

        return result;
      }

    }





    /// <summary>
    /// recursive search transform through whole database of transforms.
    /// This function builds tree with as many subpaths as many possible transforms exists
    /// serarch ends when correct transform is found or when there is nothing more 
    /// to search througn (add to tree).
    /// search goes in reverse order, so first cs is searched as target cs of transform
    /// </summary>
    /// <param name="current"></param>
    /// <param name="currentCS"></param>
    private static void treeSearchTransform(CSTPNode current, CoordinateSystem currentCS)
    {

      if (currentCS != null)
      {
        if (currentCS == _rSourceCS)
          return;
      }
      else
        currentCS = _rSourceCS;


      CSTPNode tmp;


      // scan for currentCS
      // compare target because search is in reverse order
      foreach (SRSTransform t in Environment.Libraries.SRSTransformLibrary.Transformations)
        if (t.Target.EqualParams(currentCS, CsCompareType.SkipStorageUnits) &&
            !_csExistInTreeAsSource(current, t.Target))
        {
          tmp = new CSTPNode(current, t);
          current.childs.Add(tmp);
          // if is same as source cs then stop search at all for this tree path
          if (t.Source.EqualParams(_rTargetCS, CsCompareType.SkipStorageUnits))
          {
            _successes.Add(tmp);
            return;
          }
        }

      // if any success in search
      // and not found final transform
      // recursive call....
      if (current.childs.Count > 0)
        foreach (CSTPNode n in current.childs)
          treeSearchTransform(n, n.transform.Target);

    }






    /// <summary>
    /// searches recursively all nodes up from current level
    /// whether path contains searched coordinate system as source
    /// this will avoid circular reference and unlimited loop
    /// in search function
    /// </summary>
    /// <param name="node">start search node</param>
    /// <param name="asSource">coordinate system to search for in source of transform</param>
    /// <returns></returns>
    private static bool _csExistInTreeAsSource(CSTPNode node, CoordinateSystem asSource)
    {
      foreach (CSTPNode ch in node.childs)
        if (ch.transform.Source.EqualParams(asSource, CsCompareType.SkipStorageUnits))
          return true;

      if (node.Parent != null)
        return _csExistInTreeAsSource(node.Parent, asSource);

      return false;
    }





    /// <summary>
    /// search in transform database for direct transform between source and target
    /// </summary>
    /// <param name="sourceCS">source coordinate system</param>
    /// <param name="targetCS">target coordinate system</param>
    /// <returns>if succes returns prepared TransformList, otherwise returns null</returns>
    private static TransformList searchDirectTransform(CoordinateSystem sourceCS,
                                                 CoordinateSystem targetCS)
    {

      TransformList result;

      // go over all transform in database
      // if current transform source and target cs is same to input ones
      // get this as result
      foreach (SRSTransform t in Environment.Libraries.SRSTransformLibrary.Transformations)
        if (t.Source.EqualParams(sourceCS, CsCompareType.CompareAllParams) &&
            t.Target.EqualParams(targetCS, CsCompareType.CompareAllParams))
        {
          result = new TransformList();
          result.Add(t);
          return result;
        }
        // or may be current transform is inversible and source and target are
        // saved in database in reverse order
        // so search also in reverse order ;-)
        else if (t.IsInversible)
        {
          if (t.Source.EqualParams(targetCS, CsCompareType.CompareAllParams) &&
              t.Target.EqualParams(sourceCS, CsCompareType.CompareAllParams))
          {
            result = new TransformList();
            result.Add(t.Inverse());
            return result;
          }
        }

      // ok, not found
      // try search for direct transform where storage units might differ...
      foreach (SRSTransform t in Environment.Libraries.SRSTransformLibrary.Transformations)
        if (t.Source.EqualParams(sourceCS, CsCompareType.SkipStorageUnits) &&
            t.Target.EqualParams(targetCS, CsCompareType.SkipStorageUnits))
        {
          result = new TransformList();

          // if source units and transform.source units are different
          // add MeasureUnitTransform source to transform source
          if (!t.Source.EqualParams(sourceCS, CsCompareType.CompareAllParams)) //storage units differs
            result.Add(new MeasureUnitTransform(sourceCS, t.Source));

          // add transform itself
          result.Add(t);

          // if transform.target units and target units are different
          // add MeasureUnitTransform transform.target to target
          if (!t.Target.EqualParams(targetCS, CsCompareType.CompareAllParams)) //storage units differs
            result.Add(new MeasureUnitTransform(t.Target, targetCS));

          return result;
        }

      // ok, still not found
      // search for direct transform where units and axes might differ
      // try search for direct transform where storage units might differ
      // and axes has different orientation
      foreach (SRSTransform t in Environment.Libraries.SRSTransformLibrary.Transformations)
      {
        if (t.Source.EqualParams(sourceCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation) &&
            t.Target.EqualParams(targetCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation))
        {
          return _resolveStorageUnitsAndAxes(t, sourceCS, targetCS);
        }
        else if (t.IsInversible)
        {
          if (t.Source.EqualParams(targetCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation) &&
              t.Target.EqualParams(sourceCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation))
          {
            return _resolveStorageUnitsAndAxes(t, targetCS, sourceCS);
          }
        }
      }

      return null;
    }






    /// <summary>
    /// if input transform matches input coordinate systems except storage units and axis def.
    /// this function tries to prepare MeasureUnitTransform to convert units
    /// and also if one or both coordinate systems are projcs, prepares XYQuadrantTransform
    /// for projection
    /// </summary>
    /// <param name="t">transform to compare with</param>
    /// <param name="sourceCS">source coordinate system</param>
    /// <param name="targetCS"> target coordinate system</param>
    /// <returns>TransformList with MeasureUnitTransform and or XYQuadrantTransform and input transform or null</returns>
    private static TransformList _resolveStorageUnitsAndAxes(SRSTransform t, CoordinateSystem sourceCS, CoordinateSystem targetCS)
    {
      if (t.Source.EqualParams(sourceCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation) &&
          t.Target.EqualParams(targetCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation))
      {

        TransformList result = new TransformList();

        // if source units and transform.source units are different
        // add MeasureUnitTransform source to transform source
        if (!t.Source.EqualParams(sourceCS, CsCompareType.SkipProjectionAxesOrientation))
          result.Add(new MeasureUnitTransform(sourceCS, t.Source));

        // if source and transform.source has different axis definition
        // and both are projection
        if (!t.Source.EqualParams(sourceCS, CsCompareType.SkipStorageUnits) &&
            t.Source is ProjectedCoordinateSystem)
        {
          // try if XYQuadrantTransform is possible
          SRSTransform tmp = XYQuadrantTransform.Create((ProjectedCoordinateSystem)sourceCS, (ProjectedCoordinateSystem)t.Source);
          if (tmp != null)
            result.Add(tmp);
          else
            return null;
        }

        // add transform itself
        result.Add(t);


        // if source and transform.source has different axis definition
        // and both are projection
        if (!t.Target.EqualParams(targetCS, CsCompareType.SkipStorageUnits) &&
            t.Target is ProjectedCoordinateSystem)
        {
          // try if XYQuadrantTransform is possible
          SRSTransform tmp = XYQuadrantTransform.Create((ProjectedCoordinateSystem)t.Target, (ProjectedCoordinateSystem)targetCS);
          if (tmp != null)
            result.Add(tmp);
          else
            return null;
        }

        // if transform.target units and target units are different
        // add MeasureUnitTransform transform.target to target
        if (!t.Target.EqualParams(targetCS, CsCompareType.SkipProjectionAxesOrientation))
          result.Add(new MeasureUnitTransform(t.Target, targetCS));

        return result;
      }

      return null;
    }







    /// <summary>
    /// if sourceCS matches targetCS except storage units and axis def.
    /// this function tries to prepare MeasureUnitTransform to convert units
    /// and also if both coordinate systems are projcs, prepares XYQuadrantTransform
    /// for projection. 
    /// </summary>
    /// <param name="sourceCS">source coordinate system</param>
    /// <param name="targetCS"> target coordinate system</param>
    /// <returns>TransformList with MeasureUnitTransform and or XYQuadrantTransform or null</returns>
    private static TransformList _resolveStorageUnitsAndAxes(CoordinateSystem sourceCS, CoordinateSystem targetCS)
    {
      if (sourceCS.EqualParams(targetCS, CsCompareType.SkipStorageUnits | CsCompareType.SkipProjectionAxesOrientation))
      {

        TransformList result = new TransformList();

        // if sourceCS units and targetCS units are different
        // add MeasureUnitTransform sourceCS to targetCS
        if (!sourceCS.EqualParams(targetCS, CsCompareType.SkipProjectionAxesOrientation))
          result.Add(new MeasureUnitTransform(sourceCS, targetCS));

        // if sourceCS and targetCS has different axis definition
        // and both are projection
        if (!targetCS.EqualParams(sourceCS, CsCompareType.SkipStorageUnits) &&
            targetCS is ProjectedCoordinateSystem && sourceCS is ProjectedCoordinateSystem)
        {
          // try if XYQuadrantTransform is possible
          SRSTransform tmp = XYQuadrantTransform.Create((ProjectedCoordinateSystem)sourceCS, (ProjectedCoordinateSystem)targetCS);
          if (tmp != null)
            result.Add(tmp);
          else
            return null;
        }

        return result;

      }

      return null;
    }





    private static SRSTransform getProjection(ProjectedCoordinateSystem pcs, bool getAsInverse)
    {
      ProjectionTransform pt = null;

      foreach(KeyValuePair<string, CustomComponent> kvp in Petra.Gis.Environment.Components["ProjectionTransforms"])
        if (kvp.Key.ToLower() == pcs.Projection.ClassName.ToLower())
          pt = (ProjectionTransform)kvp.Value.CreateInstance(new object[] {pcs, getAsInverse});

      if(pt == null)
        throw new ArgumentException("Could not create projection transform for : " +
                                     pcs.Projection.ClassName);

      // if names and orientation and dimension is same
      // just return this transform
      if (pt.Axes.EqualParams(pcs.Axes, CsCompareType.CompareAllParams))
      {
        return pt;
      }
      else
      {
        // if axes are totally different throw exception
        if (!pt.Axes.EqualParams(pcs.Axes, CsCompareType.SkipProjectionAxesOrientation))
          throw new ArgumentException("invalid cs def");

        // axes are same but orientation is different
        // must prepare XYQuadrantTransform
        // clone cs and prepare new axes
        ProjectedCoordinateSystem tmpPcs = (ProjectedCoordinateSystem)pcs.Clone();
        tmpPcs.Axes = pt.Axes;

        // for XYQuadrantTransform is no matter what is source and what is target...
        XYQuadrantTransform tmpQt = XYQuadrantTransform.Create(pcs, tmpPcs);
        if (tmpQt == null)
          throw new ArgumentException("invalid axis def");

        TransformList tmpList = new TransformList();
        // add projection transform first
        tmpList.Add(pt);

        if (getAsInverse)
          tmpList.Add(tmpQt);
        else
          tmpList.Insert(0, tmpQt);

        ChainedTransform result = new ChainedTransform();
        result.CoordinateTransformList = tmpList;

        return result;
      }
    }




    #endregion private members




  }

}
